﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace XmlSpecParser
{
	public class DotNetTranslator
	{
		public class Options
		{
			public string Prefix { get; set; }

			public Func<XmlCommandData, bool> CommandFilter { get; set; }
		}

		private static string[] vendorExtensions = new string[] { "3DFX", "3DL", "AMD", "APPLE", "ATI", "GREMEDY", "HP", "I3D", "IBM", "INGR", "INTEL", "MESA",
			                                                      "MESAX", "NV", "OES", "OML", "PGI", "REND", "S3", "SGI", "SGIS", "SGIX", "SUN", "SUNX", "WIN" };

		public DotNetApiData Translate(XmlSpecData spec, Options options)
		{
			DotNetApiData api = new DotNetApiData();

			this.TranslateEnums(spec, api, options);
            this.TranslateEnumGroups(spec, api, options);
            this.TranslateFunctions(spec, api, options);
			
			return api;
		}

		#region Enums

		private string InflectEnumName(string input, Options options)
		{
			string[] parts = input.Substring(options.Prefix.Length + 1).Split('_');
			string[] temp = new string[parts.Length];

			for (int i = 0; i < parts.Length; i++)
			{
				if (parts[i].Length > 0)
				{
					int capitalizeLength = 1;

					if (parts[i].Length > 1 && char.IsDigit(parts[i][0]))
						capitalizeLength = 2;

					temp[i] = parts[i].Substring(0, capitalizeLength).ToUpper() + parts[i].Substring(capitalizeLength).ToLower();
				}
			}

			string name = string.Join(string.Empty, temp);

			if (char.IsDigit(name[0]))
				name = "GL" + name;

			return name;
		}

		private bool ShouldWriteEnum(string name)
		{
            name = name.ToUpper();

            if (name.EndsWith("_ARB") || name.EndsWith("_EXT"))
                return false;

			foreach (string vendorExtension in vendorExtensions)
			{
                if (name.EndsWith("_" + vendorExtension))
					return false;
			}

			return true;
		}

		private void TranslateEnums(XmlSpecData spec, DotNetApiData api, Options options)
		{			
			foreach (var specEnum in spec.Enums.Where(x => this.ShouldWriteEnum(x.Name)))
			{
				var enumData = new DotNetEnumData()
				{
					OriginalName = specEnum.Name,
					Name = this.InflectEnumName(specEnum.Name, options),
					Value = specEnum.Value,
                    Type = specEnum.Value.Length <= 6 ? "uint" : "ulong"
				};

                if (enumData.Value.StartsWith("-"))
                    enumData.Value = string.Format("unchecked((uint){0})", enumData.Value);

				api.Enums.Add(enumData);
			}
		}

        private bool ShouldWriteEnumGroup(string name)
        {
            foreach (string vendorExtension in vendorExtensions)
            {
                if (name.ToUpper().EndsWith(vendorExtension))
                    return false;
            }

            return true;
        }

        private void TranslateEnumGroups(XmlSpecData spec, DotNetApiData api, Options options)
        {
            foreach (var specEnumGroup in spec.EnumGroups.Where(x => this.ShouldWriteEnumGroup(x.Name)))
            {
                var enumGroupData = new DotNetEnumGroupData()
                {
                    Name = specEnumGroup.Name,
                };

                foreach (var enumName in specEnumGroup.Enums.Where(x => this.ShouldWriteEnum(x)).Distinct())
                {
                    var @enumData = api.Enums.SingleOrDefault(x => x.OriginalName == enumName);

                    if (@enumData == null)
                        continue;

                    enumGroupData.Enums.Add(@enumData);

                    if (spec.Enums.Single(x => x.Name == enumName).Type == "bitmask")
                        enumGroupData.IsFlags = true;
                }

                api.EnumGroups.Add(enumGroupData);
            }
        }

		#endregion
		
		#region Functions

        public bool IsTypeEnum(XmlCommandParamData param, DotNetApiData api)
		{
			return (param.Type == "GLenum" || param.Type == "GLbitfield") &&
                   !string.IsNullOrEmpty(param.TypeGroup) &&
                   api.EnumGroups.Any(x => x.Name == param.TypeGroup);
		}

		public bool IsTypePointer(string type)
		{
			return type.Contains('*');
		}

		public bool IsTypeOutput(string type)
		{
			return this.IsTypePointer(type) && !type.Contains("const");
		}

		private bool ShouldUseGenericsForType(string type)
		{
			if (type.EndsWith("void*"))
				return true;

			//if (param.HasFlagCompSize && param.Type == "IntPtr")
			//	return true;

			return false;
		}

        private bool ShouldUseFixedForParam(XmlCommandParamData param)
        {
            string dotNetType = this.InflectFunctionParamDotNetType(param);

            if (this.IsTypePointer(param.Type) &&
                dotNetType != "string" &&
                dotNetType != "string[]" &&
                dotNetType != "StringBuilder" &&
                dotNetType != "IntPtr")
            {
                return true;
            }
            
            return false;
        }

		private bool ShouldUseAddressOfOperatorForParam(XmlCommandParamData param)
		{
            if (this.IsTypePointer(param.Type))
                return false;

			if (!string.IsNullOrEmpty(param.Length))
				return true;

			return false;
		}

		private string InflectFunctionNativeName(string input, Options options)
		{
            if (input.StartsWith(options.Prefix))
                input = input.Substring(options.Prefix.Length);

			return input;
		}

		private string InflectFunctionDotNetName(string input, Options options)
		{
			input = this.InflectFunctionNativeName(input, options);

			// Endings not to trim
			if (input.EndsWith("Access") ||
				input.EndsWith("Address") ||
				input.EndsWith("Arrays") ||
				input.EndsWith("Attrib") ||
                input.EndsWith("Bitmaps") ||
                input.EndsWith("Boolean") ||
				input.EndsWith("Buffers") ||
				input.EndsWith("Coord") ||
				input.EndsWith("Elements") ||
				input.EndsWith("End") ||
                input.EndsWith("Entries") ||
				input.EndsWith("Env") ||
				input.EndsWith("Flag") ||
				input.EndsWith("IDs") ||
				input.EndsWith("Indexed") ||
                input.EndsWith("Indices") ||
				input.EndsWith("Instanced") ||
				input.EndsWith("Pixels") ||
				input.EndsWith("Status") ||
				input.EndsWith("Tess") ||
				input.EndsWith("Textures") ||
				input.EndsWith("Uniforms") ||
				input.EndsWith("Varyings"))
			{
				return input;
			}

			if (input.EndsWith("i_v") || input.EndsWith("ubv") || input.EndsWith("uiv") || input.EndsWith("usv"))
			{
				input = input.Substring(0, input.Length - 3);
			}
			else if (input.EndsWith("bv") || input.EndsWith("dv") || input.EndsWith("fv") || input.EndsWith("iv") || input.EndsWith("sv") ||
					 input.EndsWith("ui"))
			{
				input = input.Substring(0, input.Length - 2);
			}
			else if (input.EndsWith("b") || input.EndsWith("d") || input.EndsWith("f") || input.EndsWith("i") || input.EndsWith("s") || input.EndsWith("v"))
			{
				input = input.Substring(0, input.Length - 1);
			}

			return input;
		}

		private string InflectFunctionParamName(string input)
		{
			switch (input)
			{
				case "params": input = "@params"; break;
				case "ref": input = "@ref"; break;
				case "base": input = "@base"; break;
				case "string": input = "@string"; break;
			}

			return input;
		}

		private string InflectType(string type, bool keepPointers)
		{
			if (type.StartsWith("const"))
				type = type.Substring("const ".Length);

			// Edge cases
			if (type == "GLintptr" || type == "GLintptr*" || type == "GLsizeiptr" || type == "GLsizeiptr*" || type == "GLsync")
			{
				return "IntPtr";
			}		
			else if (type == "GLchar**" || type == "char**")
			{
				return "string[]";
			}
			else if (type == "GLchar*" || type == "char*" || type == "LPCSTR")
			{
				return "string";
			}
            
			string pointer = string.Empty;

			if (type.EndsWith("**"))
			{
				type = type.Substring(0, type.Length - 2);
				pointer = "**";
			}
			else if (type.EndsWith("*"))
			{
				type = type.Substring(0, type.Length - 1);
				pointer = "*";
			}
			
			switch (type)
			{
                case "BOOL":
                    type = "bool";
                    break;

                case "FLOAT":
                    type = "float";
                    break;

                case "HANDLE":
                case "HDC":
                case "HENHMETAFILE":
                case "HGLRC":
                case "PROC":
                    type = "IntPtr";
                    break;

                case "LAYERPLANEDESCRIPTOR":
                    type = "LayerPlaneDescriptor";
                    break;
                    
                case "LPGLYPHMETRICSFLOAT":
                    type = "GlyphMetricsFloat";
                    break;
                
                case "PIXELFORMATDESCRIPTOR":
                    type = "PixelFormatDescriptor";
                    break;

                case "COLORREF":
                case "DWORD":
                case "UINT":
                    type = "uint";
                    break;

				case "GLDEBUGPROC":
					type = "Action"; // TODO: Determine correct .NET type for GLDEBUGPROC.
					break;

				case "GLboolean":
					type = "bool";
					break;

				case "GLubyte":
					type = "byte";
					break;

				case "GLdouble":
					type = "double";
					break;

				case "GLfloat":
					type = "float";
					break;

				case "GLint":
				case "GLsizei":
					type = "int";
					break;
									
				case "GLint64":
					type = "long";
					break;

				case "GLbyte":
					type = "sbyte";
					break;

				case "GLshort":
					type = "short";
					break;

				case "GLbitfield":
				case "GLenum":
				case "GLuint":
					type = "uint";
					break;

				case "GLuint64":
					type = "ulong";
					break;

				case "GLushort":
					type = "ushort";
					break;
			}

			if (keepPointers)
				type += pointer;

			return type;
		}

		private string InflectNativeReturnType(XmlCommandData command)
		{
			string type = command.ReturnType;

            if (command.ReturnTypeGroup == "ErrorCode")
                return command.ReturnTypeGroup;

			switch (command.ReturnType)
			{			
				default:
					type = this.InflectType(command.ReturnType, false);
					break;
			}

			return type;
		}

		private string InflectDotNetReturnType(XmlCommandData command)
		{
			string type = command.ReturnType;

            if (command.ReturnTypeGroup == "ErrorCode")
                return command.ReturnTypeGroup;

			switch (command.ReturnType)
			{				
				default:
					type = this.InflectType(type, false);
					break;
			}
                        
			return type;
		}

		private string InflectFunctionParamNativeType(XmlCommandParamData param)
		{								
			string type = param.Type;

            if (type == "const GLchar*")
                return "string";

			if (type.StartsWith("const"))
				type = type.Substring("const ".Length);

			switch (type)
			{			
				case "GLchar*":
					type = "StringBuilder";
					break;

				case "void*": 
				case "GLvoid*":
				case "void**":
				case "GLvoid**":
					type = "IntPtr";
					break;

				default:
					type = this.InflectType(type, true);
					break;
			}
			
			return type;
		}

		private string InflectFunctionParamDotNetType(XmlCommandParamData param)
		{
			string type = param.Type;

			switch (param.Type)
			{
                case "const GLchar**":
                    type = "string[]";
                    break;

                case "const GLchar*":
                    type = "string";
                    break;

				case "GLchar*":
					type = "StringBuilder";
					break;

                case "void*":
                case "GLvoid*":
                case "void**":
                case "GLvoid**":
                case "const GLvoid**":
                    type = "IntPtr";
                    break;
                    												
				default:
					type = this.InflectType(type, false);
					break;
			}

			if (this.ShouldUseGenericsForType(param.Type))
			{
				type = type
                    .Replace("GLvoid", "T")
                    .Replace("void", "T");
			}

			if (this.IsTypePointer(param.Type) && 
                type != "string" &&
                type != "string[]" && 
                type != "StringBuilder" && 
                type != "IntPtr")
			{
				type += "[]";
			}

			return type;
		}
                
		private void TranslateFunctions(XmlSpecData spec, DotNetApiData api, Options options)
		{			
			foreach (var specCommand in spec.Commands.Where(x => options.CommandFilter(x)))
			{
				var specFeature = spec.Features.Where(x => x.Commands.Contains(specCommand.Name)).FirstOrDefault();

				var functionData = new DotNetFunctionData()
				{
                    IsNative = true,
					OriginalName = specCommand.Name,
					NativeName = this.InflectFunctionNativeName(specCommand.Name, options),
					DotNetName = this.InflectFunctionDotNetName(specCommand.Name, options),
					OriginalReturnType = specCommand.ReturnType,
					NativeReturnType = this.InflectNativeReturnType(specCommand),
					DotNetReturnType = this.InflectDotNetReturnType(specCommand)
				};

                if (specFeature != null)
                {
                    functionData.VersionMajor = specFeature.VersionMajor;
					functionData.VersionMinor = specFeature.VersionMinor;
                    functionData.CanPInvoke = specFeature.VersionMajor == 1 && specFeature.VersionMinor <= 1;
                }

				if (functionData.NativeReturnType == "string")
					functionData.IsUnsafe = true;

				foreach (var specCommandParam in specCommand.Params)
				{
					var functionParamData = new DotNetFunctionParamData()
					{
						OriginalName = specCommandParam.Name,
						Name = this.InflectFunctionParamName(specCommandParam.Name),
						OriginalType = specCommandParam.Type,
						NativeType = this.InflectFunctionParamNativeType(specCommandParam),
						DotNetType = this.InflectFunctionParamDotNetType(specCommandParam),
						IsPointer = this.IsTypePointer(specCommandParam.Type),
						IsOutput = this.IsTypeOutput(specCommandParam.Type),
						ShouldUseGenerics = this.ShouldUseGenericsForType(specCommandParam.Type),
                        ShouldUseFixed = this.ShouldUseFixedForParam(specCommandParam),
						ShouldUseAddressOfOperator = this.ShouldUseAddressOfOperatorForParam(specCommandParam)
					};

					if (functionParamData.IsPointer)
						functionData.IsUnsafe = true;

					if (functionParamData.ShouldUseGenerics)
						functionData.ShouldUseGenerics = true;

                    if (functionParamData.IsOutput && functionParamData.DotNetType == "IntPtr")
                    {
                        functionParamData.ShouldUseOut = true;
                    }

					functionData.Params.Add(functionParamData);
				}

				api.Functions.Add(functionData);

                // Create overload which accepts the Enum variant.
                if (specCommand.Params.Any(x => this.IsTypeEnum(x, api)))
                {
                    api.Functions.Add(this.ChangeFunctionParamsToEnums(functionData, specCommand.Params.Where(x => this.IsTypeEnum(x, api))));
                }
			}

            foreach (var functionData in api.Functions.ToArray())
            {
                var specCommand = spec.Commands.Single(x => x.Name == functionData.OriginalName);

                // Commands which take a pointer and it could be a single element.
                if (specCommand.Params.Any(x => this.ShouldChangeFunctionParamToRefOrOut(x)))
                {
                    api.Functions.Add(this.ChangeFunctionParamsToRefOrOut(functionData, specCommand.Params.Where(x => this.ShouldChangeFunctionParamToRefOrOut(x)).Select(x => x.Name)));
                }

                // Commands which take a pointer and it could be a single element.
                if (specCommand.Params.Any(x => this.ShouldChangeFunctionParamToIntPtr(x)))
                {
                    api.Functions.Add(this.ChangeFunctionParamsToIntPtr(functionData, specCommand.Params.Where(x => this.ShouldChangeFunctionParamToIntPtr(x)).Select(x => x.Name)));
                }
            }

            api.Functions.Sort((x, y) => x.OriginalName != null && y.OriginalName != null ? x.OriginalName.CompareTo(y.OriginalName) : 0);
		}

        private bool ShouldChangeFunctionParamToIntPtr(XmlCommandParamData param)
        {
            return !string.IsNullOrEmpty(param.Length) &&
                   this.ShouldUseGenericsForType(param.Type) &&
                   this.ShouldUseFixedForParam(param);
        }

        private DotNetFunctionData ChangeFunctionParamsToIntPtr(DotNetFunctionData function, IEnumerable<string> @params)
        {
            var copy = function.Clone();
            copy.IsNative = false;

            foreach (var param in copy.Params.Where(x => @params.Contains(x.OriginalName)))
            {
                param.DotNetType = "IntPtr";
                param.ShouldUseGenerics = false;
                param.ShouldUseFixed = false;
            }

            if (!copy.Params.Any(x => x.IsPointer))
                copy.IsUnsafe = false;

            if (!copy.Params.Any(x => x.ShouldUseGenerics))
                copy.ShouldUseGenerics = false;

            return copy;
        }

        private bool ShouldChangeFunctionParamToRefOrOut(XmlCommandParamData param)
        {
            return !string.IsNullOrEmpty(param.Length) &&
                   !param.Type.Contains("GLchar*") &&
                   this.InflectFunctionParamDotNetType(param) != "IntPtr";
        }

        private DotNetFunctionData ChangeFunctionParamsToRefOrOut(DotNetFunctionData function, IEnumerable<string> @params)
        {
            var copy = function.Clone();
            copy.IsNative = false;

            foreach (var param in copy.Params.Where(x => @params.Contains(x.OriginalName)))
            {
                param.DotNetType = param.DotNetType.Replace("[]", "");

                if (param.IsOutput)
                {
                    param.ShouldUseOut = true;
                }
                else
                {
                    param.ShouldUseRef = true;
                }
                
                param.ShouldUseAddressOfOperator = true;
            }

            return copy;
        }

        private DotNetFunctionData ChangeFunctionParamsToEnums(DotNetFunctionData function, IEnumerable<XmlCommandParamData> @params)
        {
            var copy = function.Clone();
            copy.IsNative = false;

            foreach (var param in @params)
            {
                var dotNetParam = copy.Params.Single(x => x.OriginalName == param.Name);

                dotNetParam.DotNetType = param.TypeGroup;
                dotNetParam.IsEnum = true;
            }

            return copy;
        }

		#endregion
	}
}
